package wsdl;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Iterator;
import java.util.LinkedList;

import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.ElementHandler;
import org.dom4j.ElementPath;
import org.dom4j.io.SAXReader;

import wsdl.service.WSDLBinding;
import wsdl.service.WSDLOperation;
import wsdl.service.WSDLPort;
import wsdl.service.WSDLPortType;
import wsdl.service.WSDLService;
import bpel.BPELProcess;
import bpel.types.BPELComplexType;
import bpel.types.BPELElement;
import bpel.types.BPELMessage;
import bpel.types.BPELPart;

public class WSDLParser {

	/* Nodes */
	public static final String NODE_DEFS = "definitions";
	public static final String NODE_TYPE_LIST = "types";
	public static final String NODE_SCHEMA = "schema";
	public static final String NODE_COMPLEX_TYPE = "complexType";
	public static final String NODE_ELEMENT = "element";
	public static final String NODE_SEQUENCE = "sequence";
	public static final String NODE_MESSAGE = "message";
	public static final String NODE_PART = "part";
	public static final String NODE_PORT_TYPE = "portType";
	public static final String NODE_PORT = "port";
	public static final String NODE_BINDING = "binding";
	public static final String NODE_OPERATION = "operation";
	public static final String NODE_INPUT = "input";
	public static final String NODE_OUTPUT = "output";
	public static final String NODE_SERVICE = "service";
	public static final String NODE_DOCUMENTATION = "documentation";
	public static final String NODE_ADDRESS = "address";

	/* Attributes */
	public static final String ATTR_NAME = "name";
	public static final String ATTR_LOCATION = "location";
	public static final String ATTR_BINDING = "binding";
	public static final String ATTR_TYPE = "type";
	public static final String ATTR_VERB = "verb";
	public static final String ATTR_ELEMENT = "element";
	public static final String ATTR_NILLABLE = "nillable";
	public static final String ATTR_TARGET_NAMESPACE = "targetNamespace";
	public static final String ATTR_ELEM_FORM_DEFAULT = "elementFormDefault";
	public static final String ATTR_MIN_OCCURS = "minOccurs";
	public static final String ATTR_MAX_OCCURS = "maxOccurs";

	/* Handlers */
	private ElementHandler handlerDefs;
	private ElementHandler pruner;

	/* Result */
	private BPELProcess process;

	public WSDLParser(BPELProcess process) {

		this.process = process;

		pruner = new ElementHandler() {
			public void onStart(ElementPath path) {
				/* nothing */
			}

			public void onEnd(ElementPath path) {
				Element row = path.getCurrent();

				// Pruning
				row.detach();
			}
		};

		handlerDefs = new ElementHandler() {
			public void onStart(ElementPath path) {
				/* nothing */
			}

			public void onEnd(ElementPath path) {
				Element row = path.getCurrent();

				parseDefinitions(row);

				// Pruning
				row.detach();
			}
		};
	};

	public void parse(String WSDLFile) throws DocumentException {

		SAXReader saxReader = new SAXReader();

		saxReader.addHandler("/" + NODE_DEFS, handlerDefs);
		saxReader.setDefaultHandler(pruner);

		saxReader.read(WSDLFile);
	}

	@SuppressWarnings("unchecked")
	private void parseDefinitions(Element defsElem) {

		for (Iterator i = defsElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().contains(NODE_TYPE_LIST)) {
				parseTypeList(element);
			}

			if (element.getName().contains(NODE_MESSAGE)) {
				parseMessage(element);
			}

			if (element.getName().contains(NODE_SERVICE)) {
				parseService(element);
			}

			if (element.getName().contains(NODE_BINDING)) {
				parseBinding(element);
			}

			if (element.getName().contains(NODE_PORT_TYPE)) {
				parsePortType(element);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private void parseTypeList(Element typeListElem) {

		for (Iterator i = typeListElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().contains(NODE_SCHEMA)) {
				parseSchema(element);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private void parseSchema(Element schemaElem) {
		for (Iterator i = schemaElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().contains(NODE_COMPLEX_TYPE)) {
				BPELComplexType ct = parseComplexType(element);
				process.addType(ct.getName(), ct);
			}

			if (element.getName().contains(NODE_ELEMENT)) {
				BPELElement e = parseElement(element);
				process.addType(e.getName(), e);
			}
		}
	}

	@SuppressWarnings("unchecked")
	private BPELComplexType parseComplexType(Element ctElem) {

		String name = ctElem.attributeValue(ATTR_NAME);

		BPELComplexType ct = new BPELComplexType(name);

		for (Iterator i = ctElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().toLowerCase().contains(NODE_SEQUENCE)) {
				ct.addElements(parseSequence(element));
			}
		}

		return ct;
	}

	@SuppressWarnings("unchecked")
	private LinkedList<BPELElement> parseSequence(Element seqElem) {
		LinkedList<BPELElement> elements = new LinkedList<BPELElement>();

		for (Iterator i = seqElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().contains(NODE_ELEMENT)) {
				elements.add(parseElement(element));
			}
		}

		return elements;
	}

	@SuppressWarnings("unchecked")
	private BPELElement parseElement(Element elemElem) {

		String name = elemElem.attributeValue(ATTR_NAME);
		String type = elemElem.attributeValue(ATTR_TYPE);
		String nillable = elemElem.attributeValue(ATTR_NILLABLE);
		String min = elemElem.attributeValue(ATTR_MIN_OCCURS);
		String max = elemElem.attributeValue(ATTR_MAX_OCCURS);

		BPELElement e = new BPELElement(name, type, (nillable != null && nillable.equals("true")), (min != null ? Integer.parseInt(min) : -1), (max != null ? Integer.parseInt(min) : -1));

		if (type == null) {
			for (Iterator i = elemElem.elementIterator(); i.hasNext();) {

				Element element = (Element) i.next();

				if (element.getName().contains(NODE_COMPLEX_TYPE)) {
					e.setSubtype(parseComplexType(element));
				}
			}
		}

		return e;
	}

	@SuppressWarnings("unchecked")
	private void parseMessage(Element msgElem) {
		String name = msgElem.attributeValue(ATTR_NAME);

		BPELMessage msg = new BPELMessage(name);

		for (Iterator i = msgElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().contains(NODE_PART)) {
				BPELPart part = parsePart(element);
				msg.addPart(part.getName(), part);
			}
		}

		process.addType(msg.getName(), msg);
	}

	private BPELPart parsePart(Element partElem) {

		String name = partElem.attributeValue(ATTR_NAME);
		String element = partElem.attributeValue(ATTR_ELEMENT);

		return new BPELPart(name, element);
	}

	@SuppressWarnings("unchecked")
	private void parseService(Element serviceElem) {

		String name = serviceElem.attributeValue(ATTR_NAME);

		WSDLService service = new WSDLService(name);

		for (Iterator i = serviceElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().contains(NODE_PORT)) {
				WSDLPort port = parsePort(element);
				service.addPort(port.getName(), port);
			}
		}

		process.addService(service.getName(), service);
	}

	@SuppressWarnings("unchecked")
	private WSDLPort parsePort(Element portElem) {

		String name = portElem.attributeValue(ATTR_NAME);
		String binding = portElem.attributeValue(ATTR_BINDING);

		WSDLPort port = new WSDLPort(name, binding);

		for (Iterator i = portElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().contains(NODE_ADDRESS)) {
				try {
					String urlString = element.attributeValue(ATTR_LOCATION);
					URL url = new URL(urlString);

					port.setAddress(url);

				} catch (MalformedURLException e) {
					e.printStackTrace();
				}
			}
		}

		return port;
	}

	@SuppressWarnings("unchecked")
	private void parseBinding(Element bindingElem) {
		String name = bindingElem.attributeValue(ATTR_NAME);
		String type = bindingElem.attributeValue(ATTR_TYPE);

		WSDLBinding binding = new WSDLBinding(name, type);

		for (Iterator i = bindingElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().contains(NODE_BINDING)) {
				binding.setVerb(element.attributeValue(ATTR_VERB));
			}

			if (element.getName().contains(NODE_OPERATION)) {
				WSDLOperation operation = parseOperation(element);
				binding.addOperation(operation.getName(), operation);
			}
		}

		process.addBinding(name, binding);
	}

	@SuppressWarnings("unchecked")
	private void parsePortType(Element portTypeElem) {
		String name = portTypeElem.attributeValue(ATTR_NAME);

		WSDLPortType portType = new WSDLPortType(name);

		for (Iterator i = portTypeElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().contains(NODE_OPERATION)) {
				WSDLOperation operation = parseOperation(element);
				portType.addOperation(operation.getName(), operation);
			}
		}

		process.addPortType(name, portType);
	}

	@SuppressWarnings("unchecked")
	private WSDLOperation parseOperation(Element operationElem) {
		String name = operationElem.attributeValue(ATTR_NAME);

		WSDLOperation operation = new WSDLOperation(name);

		for (Iterator i = operationElem.elementIterator(); i.hasNext();) {

			Element element = (Element) i.next();

			if (element.getName().contains(NODE_DOCUMENTATION)) {
				operation.setDocumentation(element.getStringValue());
			}

			if (element.getName().contains(NODE_OPERATION)) {
				operation.setLocation(element.attributeValue(ATTR_LOCATION));
			}

			// TODO Parse input & output
		}

		return operation;
	}
}
